/*
 * Brisk
 *
 * Cross-platform application framework
 * --------------------------------------------------------------
 *
 * Copyright (C) 2025 Brisk Developers
 *
 * This file is part of the Brisk library.
 *
 * Brisk is dual-licensed under the GNU General Public License, version 2 (GPL-2.0+),
 * and a commercial license. You may use, modify, and distribute this software under
 * the terms of the GPL-2.0+ license if you comply with its conditions.
 *
 * You should have received a copy of the GNU General Public License along with this program.
 * If not, see <http://www.gnu.org/licenses/>.
 *
 * If you do not wish to be bound by the GPL-2.0+ license, you must purchase a commercial
 * license. For commercial licensing options, please visit: https://brisklib.com
 */
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <string_view>
#include <brisk/core/Compression.hpp>
#include <brisk/core/Text.hpp>

#include <brisk/core/internal/Resources.h>

// pack_resource --c output.c input.bin
// pack_resource --br --c Ident output.c input.bin
// pack_resource --gz --c Ident output.c input.bin
// pack_resource --lz4 --c Ident output.c input.bin
// pack_resource --br output.br input.bin
// pack_resource --gz output.gz input.bin
// pack_resource --lz4 output.lz4 input.bin

inline void shift(int& argc, const char**& argv) {
    ++argv;
    --argc;
}

namespace Brisk {

using Internal::ResourceCompression;

using namespace std::string_view_literals;

class CWriter final : public Writer {
public:
    CWriter(Rc<Stream> dataWriter, ResourceCompression compression, std::string ident)
        : dataWriter(std::move(dataWriter)), ident(std::move(ident)) {

        std::ignore = this->dataWriter->write(
            R"(/* Autogenerated by pack_resource */
#include <brisk/core/internal/incbin.h>

#ifndef __INTELLISENSE__
#ifdef __cplusplus
extern "C" {
#endif
)");

        std::ignore = this->dataWriter->write(
            fmt::format(R"(INCBIN_CONST INCBIN_ALIGN unsigned char rsrc__{0}_data[] = {{
)",
                        this->ident));
    }

    Transferred write(const std::byte* data, size_t size) final {
        for (size_t i = 0; i < size; ++i) {
            bool firstOnLine = numWritten % 16 == 0;
            if (numWritten && firstOnLine) {
                if (this->dataWriter->write(",\n").isError())
                    return Transferred::Error;
            }
            if (this->dataWriter
                    ->write(fmt::format(fmt::runtime(firstOnLine ? "0x{:02X}" : ",0x{:02X}"),
                                        static_cast<uint8_t>(data[i])))
                    .isError()) {
                return Transferred::Error;
            }
            ++numWritten;
        }
        return size;
    }

    bool flush() final {
        if (numWritten == 0) {
            std::ignore = this->dataWriter->write(fmt::format(R"( 0 }};
INCBIN_CONST INCBIN_ALIGN unsigned char *const rsrc__{0}_end = rsrc__{0}_data;
INCBIN_CONST unsigned int rsrc__{0}_size = 0;
#ifdef __cplusplus
}}
#endif
#endif
)",
                                                              this->ident));
        } else {
            std::ignore = this->dataWriter->write(fmt::format(R"(}};
INCBIN_CONST INCBIN_ALIGN unsigned char *const rsrc__{0}_end = rsrc__{0}_data + sizeof(rsrc__{0}_data);
INCBIN_CONST unsigned int rsrc__{0}_size = {1};
#ifdef __cplusplus
}}
#endif
#endif
)",
                                                              this->ident, numWritten));
        }
        fmt::println("Output size: {}", numWritten);
        return true;
    }

    Rc<Stream> dataWriter;
    std::string ident;
    ResourceCompression compression;
    size_t numWritten = 0;
};

CompressionMethod method        = CompressionMethod::None;
CompressionLevel level          = CompressionLevel::High;
ResourceCompression compression = ResourceCompression::None;
std::string cIdent;

int pack_resource(int argc, const char** argv) {

    shift(argc, argv);

    if (argc < 2) {
        fprintf(stderr, "pack_resource requires at least two arguments: <output file> <input file>\n");
        return 1;
    }
    for (;;) {
        if (argv[0] == "--gz"sv) {
            method      = CompressionMethod::GZip;
            compression = ResourceCompression::GZip;
            shift(argc, argv);
        } else if (argv[0] == "--br"sv) {
#ifdef BRISK_HAVE_BROTLI
            method      = CompressionMethod::Brotli;
            compression = ResourceCompression::Brotli;
#else
            fprintf(stderr, "Brotli support is disabled during the build\n");
            return 1;
#endif
            shift(argc, argv);
        } else if (argv[0] == "--zlib"sv) {
            method      = CompressionMethod::ZLib;
            compression = ResourceCompression::ZLib;
            shift(argc, argv);
        } else if (argv[0] == "--lz4"sv) {
            method      = CompressionMethod::LZ4;
            compression = ResourceCompression::LZ4;
            shift(argc, argv);
        } else if (argv[0] == "--c"sv) {
            shift(argc, argv);
            if (argc < 2) {
                fprintf(stderr, "--c requires an argument\n");
                return 1;
            }
            cIdent = argv[0];
            shift(argc, argv);
        } else if (argv[0][0] == '-' && "123456789"sv.find(argv[0][1]) != std::string_view::npos) {
            level = static_cast<CompressionLevel>(argv[0][1] - '0');
            shift(argc, argv);
        } else {
            break;
        }
    }

    if (argc != 2) {
        fprintf(stderr,
                "pack_resource requires exactly two positional arguments: <output file> <input file>\n");
        return 1;
    }

    fs::path datafile = argv[0];
    fs::path input    = argv[1];

    if (auto rd = openFileForReading(input)) {
        if (auto wr = openFileForWriting(datafile)) {
            fmt::println("Input size: {}", (*rd)->size());
            Rc<Stream> out = std::move(*wr);
            if (!cIdent.empty()) {
                out.reset(new CWriter(std::move(out), compression, cIdent));
            }
            switch (method) {
#ifdef BRISK_HAVE_BROTLI
            case CompressionMethod::Brotli:
                out = brotliEncoder(std::move(out), level);
                break;
#endif
            case CompressionMethod::GZip:
                out = gzipEncoder(std::move(out), level);
                break;
            case CompressionMethod::LZ4:
                out = lz4Encoder(std::move(out), level);
                break;
            default:
                break;
            }
            if (!writeFromReader(out, *rd, 8192)) {
                fprintf(stderr, "File writing incomplete\n");
                return 1;
            }
        } else {
            fprintf(stderr, "Cannot open the data file for writing: %s\n", datafile.string().c_str());
            return 1;
        }
    } else {
        fprintf(stderr, "Cannot open the input file for reading: %s\n", input.string().c_str());
        return 1;
    }

    return 0;
}
} // namespace Brisk

int main(int argc, const char** argv) {
    return Brisk::pack_resource(argc, argv);
}
